//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//********************************************************* 

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;

namespace Microsoft.TridentWordAddIn.Common
{
    /// <summary>
    /// This class is the base type implemnted by all the Data Access Objects. It provides methods for 
    /// comparing and cloning objects. 
    /// </summary>
    public abstract class DomainBase : ICloneable
    {
        /// <summary>
        /// Determines whether the specified <see cref="T:System.Object"></see> is equal to the current <see cref="T:System.Object"></see>.
        /// </summary>
        /// <param name="obj">The <see cref="T:System.Object"></see> to compare with the current <see cref="T:System.Object"></see>.</param>
        /// <returns>
        /// true if the specified <see cref="T:System.Object"></see> is equal to the current <see cref="T:System.Object"></see>; otherwise, false.
        /// </returns>
        public override bool Equals(object obj)
        {
            if (obj == null)
            {
                return false;
            }

            if (this.GetType() != obj.GetType())
            {
                return false;
            }

            PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
            foreach (PropertyInfo property in properties)
            {
                Attribute attr = Attribute.GetCustomAttribute(property, typeof(DoNotCompareAttribute));

                if (attr == null)
                {
                    object value1 = property.GetValue(this, null);
                    object value2 = property.GetValue(obj, null);

                    Type type = property.PropertyType;
                    if (type.IsArray)
                    {
                        Type elementType = type.GetElementType();

                        MethodInfo compareArrayMethod =
                            this.GetType().GetMethod("CompareArray", BindingFlags.Instance | BindingFlags.NonPublic);
                        MethodInfo constructed = compareArrayMethod.MakeGenericMethod(elementType);
                        bool result = (bool)constructed.Invoke(this, new object[] { value1, value2 });

                        if (!result)
                        {
                            return false;
                        }
                    }
                    else if (property.PropertyType.IsGenericType)
                    {
                        Type[] typeArguments = property.PropertyType.GetGenericArguments();
                        bool result;
                        string methodName = string.Empty;
                        object[] args = { value1, value2 };

                        Attribute compareAsdictionary =
                            Attribute.GetCustomAttribute(property, typeof(CompareAsDictionaryAttribute));

                        string propertyTypeName = property.PropertyType.FullName;
                        if (compareAsdictionary != null ||
                            propertyTypeName.StartsWith("System.Collections.Generic.Dictionary`2"))
                        {
                            methodName = "CompareDictionaries";
                        }
                        if (propertyTypeName.StartsWith("System.Collections.Generic.ICollection`1")
                            || propertyTypeName.StartsWith("System.Collections.Generic.List`1")
                            || propertyTypeName.StartsWith("System.Collections.Generic.IList`1"))
                        {
                            methodName = "CompareCollection";
                        }

                        MethodInfo compareMethod =
                            this.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic);
                        MethodInfo constructed = compareMethod.MakeGenericMethod(typeArguments);

                        result = (bool)constructed.Invoke(this, args);

                        if (!result)
                        {
                            return false;
                        }
                    }
                    else
                    {
                        bool comparision = Equals(value1, value2);
                        if (comparision == false)
                        {
                            return false;
                        }
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Serves as a hash function for a particular type. <see cref="M:System.Object.GetHashCode"></see> is suitable for use in hashing algorithms and data structures like a hash table.
        /// </summary>
        /// <returns>
        /// A hash code for the current <see cref="T:System.Object"></see>.
        /// </returns>
        public override int GetHashCode()
        {
            int result = 0;

            PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);

            foreach (PropertyInfo property in properties)
            {
                object value = property.GetValue(this, null);

                if (value != null)
                {
                    if (result == 0)
                    {
                        result = value.GetHashCode();
                    }
                    else
                    {
                        result = result ^ value.GetHashCode();
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Copies the string.
        /// </summary>
        /// <param name="instance">The instance.</param>
        /// <returns>string</returns>
        protected internal string CopyString(string instance)
        {
            return instance != null ? string.Copy(instance) : null;
        }

        /// <summary>
        /// Compares the two arrays.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="array">The array.</param>
        /// <param name="comparedArray">The compared array.</param>
        /// <returns>true if arrays are equal, else false</returns>
        protected internal bool CompareArray<T>(T[] array, T[] comparedArray)
        {
            if (array == null && comparedArray == null)
            {
                return true;
            }

            if (array == null || comparedArray == null)
            {
                return false;
            }

            List<T> list = new List<T>();
            list.AddRange(array);
            List<T> comparedList = new List<T>();
            comparedList.AddRange(comparedArray);

            return CompareCollection<T>(list, comparedList);
        }

        /// <summary>
        /// Compares the two collection.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="collection">The collection.</param>
        /// <param name="comparedCollection">The compared collection.</param>
        /// <returns>
        /// true, if the collections are equal, else false
        /// </returns>
        protected internal bool CompareCollection<T>(ICollection<T> collection, ICollection<T> comparedCollection)
        {
            if (collection != comparedCollection)
            {
                if (collection != null && comparedCollection != null)
                {
                    if (collection.Count != comparedCollection.Count)
                    {
                        return false;
                    }

                    bool found;

                    foreach (T item in collection)
                    {
                        found = false;

                        foreach (T comparedItem in comparedCollection)
                        {
                            if (Equals(item, comparedItem))
                            {
                                found = true;
                            }

                            if (found)
                            {
                                break;
                            }
                        }

                        if (!found)
                        {
                            return false;
                        }
                    }
                }
                else
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Compares the dictionaries.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="DomainClass">The type of the omain class.</typeparam>
        /// <param name="dictionary">The dictionary.</param>
        /// <param name="comparedDictionary">The compared dictionary.</param>
        /// <returns>
        /// true, if teh dictionaries are equal, else false
        /// </returns>
        protected internal bool CompareDictionaries<T, DomainClass>(Dictionary<T, DomainClass> dictionary,
                                                     Dictionary<T, DomainClass> comparedDictionary)
        {
            if (dictionary != comparedDictionary)
            {
                if (dictionary != null && comparedDictionary != null)
                {
                    if (dictionary.Count != comparedDictionary.Count)
                    {
                        return false;
                    }

                    bool found;
                    foreach (KeyValuePair<T, DomainClass> thisKeyValuePair in dictionary)
                    {
                        found = false;

                        foreach (KeyValuePair<T, DomainClass> comparedKeyValuePair in comparedDictionary)
                        {
                            if (thisKeyValuePair.Key.Equals(comparedKeyValuePair.Key))
                            {
                                found = true;

                                if (!Equals(thisKeyValuePair.Value, comparedKeyValuePair.Value))
                                {
                                    return false;
                                }
                            }

                            if (found)
                            {
                                break;
                            }
                        }

                        if (!found)
                        {
                            return false;
                        }
                    }
                }
                else
                {
                    return false;
                }
            }

            return true;
        }

        #region ICloneable Members

        /// <summary>
        /// Creates a new object that is a copy of the current instance.
        /// </summary>
        /// <returns>
        /// A new object that is a copy of this instance.
        /// </returns>
        public virtual object Clone()
        {
            try
            {
                MethodInfo cloneMethod =
                    this.GetType().GetMethod("CloneObject", BindingFlags.Instance | BindingFlags.NonPublic);
                MethodInfo constructed = cloneMethod.MakeGenericMethod(this.GetType());
                return constructed.Invoke(this, new object[] { this });
            }
            catch (Exception e)
            {
                Trace.WriteLine(e.ToString(), TraceLevel.Error.ToString());
            }

            return null;
        }

        /// <summary>
        /// Clones the object.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="original">The original object.</param>
        /// <returns>T</returns>
        protected internal T CloneObject<T>(T original)
        {
            Type entityType = typeof(T);
            T clone = (T)entityType.Assembly.CreateInstance(entityType.FullName);

            PropertyInfo[] propertyInfos = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);

            foreach (PropertyInfo propertyInfo in propertyInfos)
            {
                Attribute doNotCloneAttr = Attribute.GetCustomAttribute(propertyInfo, typeof(DoNotCloneAttribute));
                Attribute cloneAsDictAttr = Attribute.GetCustomAttribute(propertyInfo, typeof(CloneAsDictionaryAttribute));

                if (doNotCloneAttr == null)
                {
                    MethodInfo getMethod = propertyInfo.GetGetMethod();
                    MethodInfo setMethod = propertyInfo.GetSetMethod();

                    object value = getMethod.Invoke(original, null);
                    object cloneObject = null;

                    Type type = propertyInfo.PropertyType;

                    // lists and dictionaries dont require setters, set this later for them
                    bool setMethodRequired = true;

                    if (type.IsValueType)
                    {
                        cloneObject = value;
                    }
                    else if (!type.IsValueType && value != null)
                    {
                        string propertyTypeFullName = propertyInfo.PropertyType.FullName;
                        if (type == typeof(string))
                        {
                            cloneObject = this.CopyString(value as string);
                        }
                        else if (type.IsArray)
                        {
                            Type elementType = type.GetElementType();

                            if (elementType == typeof(string))
                            {
                                cloneObject = this.CloneStringArray(value as string[]);
                            }
                            //Added cloning support for byte data type
                            if (elementType == typeof(byte))
                            {
                                cloneObject = this.CloneByteArray(value as byte[]);
                            }
                            else if (elementType.GetInterface("ICloneable") != null)
                            {
                                MethodInfo cloneArrayMethod = this.GetType().GetMethod("CloneArray", BindingFlags.Instance | BindingFlags.NonPublic);
                                MethodInfo constructed = cloneArrayMethod.MakeGenericMethod(elementType);
                                cloneObject = constructed.Invoke(this, new object[] { value });
                            }
                        }
                        else if (value is ICloneable)
                        {
                            cloneObject = ((ICloneable)value).Clone();
                        }
                        else if (propertyInfo.PropertyType.IsGenericType
                            && (propertyTypeFullName.StartsWith("System.Collections.Generic.IList`1")
                            || propertyTypeFullName.StartsWith("System.Collections.Generic.List`1")))
                        {
                            setMethodRequired = false;
                            Type[] typeArguments = propertyInfo.PropertyType.GetGenericArguments();

                            MethodInfo compareMethod = this.GetType().GetMethod("CloneList", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance);
                            MethodInfo constructed = compareMethod.MakeGenericMethod(typeArguments);
                            constructed.Invoke(this, new object[] { clone, propertyInfo });
                        }
                        else if (cloneAsDictAttr != null)
                        {
                            setMethodRequired = false;
                            // get method to do cloning
                            MethodInfo compareMethod = this.GetType().GetMethod("CloneDictionary", BindingFlags.Instance | BindingFlags.NonPublic);

                            // set generic arguments for method
                            Type[] typeArguments = propertyInfo.PropertyType.GetGenericArguments();

                            MethodInfo constructed = compareMethod.MakeGenericMethod(typeArguments);
                            cloneObject = constructed.Invoke(this, new object[] { clone, propertyInfo });
                        }
                    }

                    if (setMethod != null)
                    {
                        if (setMethodRequired)
                        {
                            setMethod.Invoke(clone, new object[] { cloneObject });
                        }
                    }
                }
            }

            return clone;
        }

        /// <summary>
        /// Clones the byte array.
        /// </summary>
        /// <param name="originals">The originals.</param>
        /// <returns>byte[]</returns>
        protected internal byte[] CloneByteArray(byte[] originals)
        {
            byte[] clones = new byte[originals.Length];

            int counter = 0;
            foreach (byte s in originals)
            {
                clones[counter++] = Byte.Parse(this.CopyString(s.ToString()));
            }

            return clones;
        }

        #endregion

        /// <summary>
        /// Clones the array.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="originals">The original array.</param>
        /// <returns>A clone of the Array passed</returns>
        protected internal T[] CloneArray<T>(T[] originals)
            where T : ICloneable
        {
            T[] clones = new T[originals.Length];

            int counter = 0;
            foreach (T t in originals)
            {
                clones[counter++] = (T)t.Clone();
            }

            return clones;
        }

        /// <summary>
        /// Clones the list.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="target">The target.</param>
        /// <param name="propInfo">The prop info.</param>
        protected internal void CloneList<T>(object target, PropertyInfo propInfo)
            where T : ICloneable
        {
            // get the IList from this object
            IList<T> sourceList = (IList<T>)propInfo.GetValue(this, null);
            IList<T> destList = (IList<T>)propInfo.GetValue(target, null);
            foreach (T t in sourceList)
            {
                destList.Add((T)t.Clone());
            }
        }

        /// <summary>
        /// Clones the dictionary.
        /// </summary>
        /// <typeparam name="TKey">The type of the key.</typeparam>
        /// <typeparam name="TValue">The type of the value.</typeparam>
        /// <param name="target">The target.</param>
        /// <param name="propInfo">The prop info.</param>
        protected internal void CloneDictionary<TKey, TValue>(object target, PropertyInfo propInfo)
            where TValue : ICloneable
        {
            IDictionary<TKey, TValue> sourceDict = (IDictionary<TKey, TValue>)propInfo.GetValue(this, null);
            IDictionary<TKey, TValue> destDict = (IDictionary<TKey, TValue>)propInfo.GetValue(target, null);
            foreach (TKey key in sourceDict.Keys)
            {
                TValue val = sourceDict[key];

                // value types just get copied
                if (key.GetType().IsValueType)
                {
                    destDict.Add(key, (TValue)val.Clone());
                }
                else if (key is ICloneable) // otherwise clone the key
                {
                    destDict.Add((TKey)((ICloneable)key).Clone(), (TValue)val.Clone());
                }
            }
        }

        /// <summary>
        /// Clones the string array.
        /// </summary>
        /// <param name="originals">The original string array.</param>
        /// <returns>string[]</returns>
        protected internal string[] CloneStringArray(string[] originals)
        {
            string[] clones = new string[originals.Length];
            int counter = 0;
            foreach (string s in originals)
            {
                clones[counter++] = this.CopyString(s);
            }

            return clones;
        }
    }

    /// <summary>
    /// Represents an attribute that should not be cloned
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public sealed class DoNotCloneAttribute : Attribute
    {
    }

    /// <summary>
    /// Represents an attribute that should be cloned as a dictionary
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public sealed class CloneAsDictionaryAttribute : Attribute
    {
    }

    /// <summary>
    /// Represents an attribute that should not be compared
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public sealed class DoNotCompareAttribute : Attribute
    {
    }

    /// <summary>
    /// Represents an attribute that should be compared as a Dictionary (The SerializedDictionary has to be compared
    /// like a Dictionary)
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public sealed class CompareAsDictionaryAttribute : Attribute
    {
    }
}
